import { Serializable, SerializedConstructor } from "../load/serializable.js";
import { ContentBlock } from "./content/index.js";
import { isDataContentBlock } from "./content/data.js";
import { convertToV1FromAnthropicInput } from "./block_translators/anthropic.js";
import { convertToV1FromDataContent } from "./block_translators/data.js";
import { convertToV1FromChatCompletionsInput } from "./block_translators/openai.js";
import {
  $InferMessageContent,
  $InferResponseMetadata,
  MessageStructure,
  MessageType,
  isMessage,
  Message,
} from "./message.js";
import {
  convertToFormattedString,
  type MessageStringFormat,
} from "./format.js";

/** @internal */
const MESSAGE_SYMBOL = Symbol.for("langchain.message");

export interface StoredMessageData {
  content: string;
  role: string | undefined;
  name: string | undefined;
  tool_call_id: string | undefined;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  additional_kwargs?: Record<string, any>;
  /** Response metadata. For example: response headers, logprobs, token counts, model name. */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  response_metadata?: Record<string, any>;
  id?: string;
}

export interface StoredMessage {
  type: string;
  data: StoredMessageData;
}

export interface StoredGeneration {
  text: string;
  message?: StoredMessage;
}

export interface StoredMessageV1 {
  type: string;
  role: string | undefined;
  text: string;
}

export type MessageContent = string | Array<ContentBlock>;

export interface FunctionCall {
  /**
   * The arguments to call the function with, as generated by the model in JSON
   * format. Note that the model does not always generate valid JSON, and may
   * hallucinate parameters not defined by your function schema. Validate the
   * arguments in your code before calling your function.
   */
  arguments: string;

  /**
   * The name of the function to call.
   */
  name: string;
}

export type BaseMessageFields<
  TStructure extends MessageStructure = MessageStructure,
  TRole extends MessageType = MessageType
> = {
  id?: string;
  name?: string;
  content?: $InferMessageContent<TStructure, TRole>;
  contentBlocks?: Array<ContentBlock.Standard>;
  /** @deprecated */
  additional_kwargs?: {
    /**
     * @deprecated Use "tool_calls" field on AIMessages instead
     */
    function_call?: FunctionCall;
    /**
     * @deprecated Use "tool_calls" field on AIMessages instead
     */
    tool_calls?: OpenAIToolCall[];
    [key: string]: unknown;
  };
  response_metadata?: Partial<$InferResponseMetadata<TStructure, TRole>>;
};

export function mergeContent(
  firstContent: MessageContent,
  secondContent: MessageContent
): MessageContent {
  // If first content is a string
  if (typeof firstContent === "string") {
    if (firstContent === "") {
      return secondContent;
    }
    if (typeof secondContent === "string") {
      return firstContent + secondContent;
    } else if (Array.isArray(secondContent) && secondContent.length === 0) {
      return firstContent;
    } else if (
      Array.isArray(secondContent) &&
      secondContent.some((c) => isDataContentBlock(c))
    ) {
      return [
        {
          type: "text",
          source_type: "text",
          text: firstContent,
        },
        ...secondContent,
      ];
    } else {
      return [{ type: "text", text: firstContent }, ...secondContent];
    }
    // If both are arrays
  } else if (Array.isArray(secondContent)) {
    return (
      _mergeLists(firstContent, secondContent) ?? [
        ...firstContent,
        ...secondContent,
      ]
    );
  } else {
    if (secondContent === "") {
      return firstContent;
    } else if (
      Array.isArray(firstContent) &&
      firstContent.some((c) => isDataContentBlock(c))
    ) {
      return [
        ...firstContent,
        {
          type: "file",
          source_type: "text",
          text: secondContent,
        },
      ];
    } else {
      return [...firstContent, { type: "text", text: secondContent }];
    }
  }
}

/**
 * 'Merge' two statuses. If either value passed is 'error', it will return 'error'. Else
 * it will return 'success'.
 *
 * @param {"success" | "error" | undefined} left The existing value to 'merge' with the new value.
 * @param {"success" | "error" | undefined} right The new value to 'merge' with the existing value
 * @returns {"success" | "error"} The 'merged' value.
 */
export function _mergeStatus(
  left?: "success" | "error",
  right?: "success" | "error"
): "success" | "error" | undefined {
  if (left === "error" || right === "error") {
    return "error";
  }
  return "success";
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function stringifyWithDepthLimit(obj: any, depthLimit: number): string {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  function helper(obj: any, currentDepth: number): any {
    if (typeof obj !== "object" || obj === null || obj === undefined) {
      return obj;
    }
    if (currentDepth >= depthLimit) {
      if (Array.isArray(obj)) {
        return "[Array]";
      }
      return "[Object]";
    }

    if (Array.isArray(obj)) {
      return obj.map((item) => helper(item, currentDepth + 1));
    }

    const result: Record<string, unknown> = {};
    for (const key of Object.keys(obj)) {
      result[key] = helper(obj[key], currentDepth + 1);
    }
    return result;
  }

  return JSON.stringify(helper(obj, 0), null, 2);
}

/**
 * Base class for all types of messages in a conversation. It includes
 * properties like `content`, `name`, and `additional_kwargs`. It also
 * includes methods like `toDict()` and `_getType()`.
 */
export abstract class BaseMessage<
    TStructure extends MessageStructure = MessageStructure,
    TRole extends MessageType = MessageType
  >
  extends Serializable
  implements Message<TStructure, TRole>
{
  lc_namespace = ["langchain_core", "messages"];

  lc_serializable = true;

  get lc_aliases(): Record<string, string> {
    // exclude snake case conversion to pascal case
    return {
      additional_kwargs: "additional_kwargs",
      response_metadata: "response_metadata",
    };
  }

  readonly [MESSAGE_SYMBOL] = true as const;

  abstract readonly type: TRole;

  id?: string;

  name?: string;

  content: $InferMessageContent<TStructure, TRole>;

  additional_kwargs: NonNullable<
    BaseMessageFields<TStructure, TRole>["additional_kwargs"]
  >;

  response_metadata: NonNullable<
    BaseMessageFields<TStructure, TRole>["response_metadata"]
  >;

  /**
   * @deprecated Use .getType() instead or import the proper typeguard.
   * For example:
   *
   * ```ts
   * import { isAIMessage } from "@langchain/core/messages";
   *
   * const message = new AIMessage("Hello!");
   * isAIMessage(message); // true
   * ```
   */
  _getType(): MessageType {
    return this.type;
  }

  /**
   * @deprecated Use .type instead
   * The type of the message.
   */
  getType(): MessageType {
    return this._getType();
  }

  constructor(
    arg:
      | $InferMessageContent<TStructure, TRole>
      | BaseMessageFields<TStructure, TRole>
  ) {
    const fields: BaseMessageFields<TStructure, TRole> =
      typeof arg === "string" || Array.isArray(arg) ? { content: arg } : arg;
    if (!fields.additional_kwargs) {
      fields.additional_kwargs = {};
    }
    if (!fields.response_metadata) {
      fields.response_metadata = {};
    }
    super(fields);
    this.name = fields.name;
    if (fields.content === undefined && fields.contentBlocks !== undefined) {
      this.content = fields.contentBlocks as $InferMessageContent<
        TStructure,
        TRole
      >;
      this.response_metadata = {
        output_version: "v1",
        ...fields.response_metadata,
      };
    } else if (fields.content !== undefined) {
      this.content = fields.content ?? [];
      this.response_metadata = fields.response_metadata;
    } else {
      this.content = [] as $InferMessageContent<TStructure, TRole>;
      this.response_metadata = fields.response_metadata;
    }
    this.additional_kwargs = fields.additional_kwargs;
    this.id = fields.id;
  }

  /** Get text content of the message. */
  get text(): string {
    if (typeof this.content === "string") {
      return this.content;
    }
    if (!Array.isArray(this.content)) return "";
    return this.content
      .map((c) => {
        if (typeof c === "string") return c;
        if (c.type === "text") return c.text;
        return "";
      })
      .join("");
  }

  get contentBlocks(): Array<ContentBlock.Standard> {
    const blocks: Array<ContentBlock> =
      typeof this.content === "string"
        ? [{ type: "text", text: this.content }]
        : this.content;
    const parsingSteps = [
      convertToV1FromDataContent,
      convertToV1FromChatCompletionsInput,
      convertToV1FromAnthropicInput,
    ];
    const parsedBlocks = parsingSteps.reduce(
      (blocks, step) => step(blocks),
      blocks
    );
    return parsedBlocks as Array<ContentBlock.Standard>;
  }

  toDict(): StoredMessage {
    return {
      type: this.getType(),
      data: (this.toJSON() as SerializedConstructor)
        .kwargs as StoredMessageData,
    };
  }

  static lc_name() {
    return "BaseMessage";
  }

  // Can't be protected for silly reasons
  get _printableFields(): Record<string, unknown> {
    return {
      id: this.id,
      content: this.content,
      name: this.name,
      additional_kwargs: this.additional_kwargs,
      response_metadata: this.response_metadata,
    };
  }

  static isInstance(obj: unknown): obj is BaseMessage {
    return (
      typeof obj === "object" &&
      obj !== null &&
      MESSAGE_SYMBOL in obj &&
      obj[MESSAGE_SYMBOL] === true &&
      isMessage(obj)
    );
  }

  // this private method is used to update the ID for the runtime
  // value as well as in lc_kwargs for serialisation
  _updateId(value: string | undefined) {
    this.id = value;

    // lc_attributes wouldn't work here, because jest compares the
    // whole object
    this.lc_kwargs.id = value;
  }

  get [Symbol.toStringTag]() {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return (this.constructor as any).lc_name();
  }

  // Override the default behavior of console.log
  [Symbol.for("nodejs.util.inspect.custom")](depth: number | null) {
    if (depth === null) {
      return this;
    }
    const printable = stringifyWithDepthLimit(
      this._printableFields,
      Math.max(4, depth)
    );
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return `${(this.constructor as any).lc_name()} ${printable}`;
  }

  toFormattedString(format: MessageStringFormat = "pretty"): string {
    return convertToFormattedString(this, format);
  }
}

/**
 * @deprecated Use "tool_calls" field on AIMessages instead
 */
export type OpenAIToolCall = {
  /**
   * The ID of the tool call.
   */
  id: string;

  /**
   * The function that the model called.
   */
  function: FunctionCall;

  /**
   * The type of the tool. Currently, only `function` is supported.
   */
  type: "function";

  index?: number;
};

export function isOpenAIToolCallArray(
  value?: unknown
): value is OpenAIToolCall[] {
  return (
    Array.isArray(value) &&
    value.every((v) => typeof (v as OpenAIToolCall).index === "number")
  );
}

export function _mergeDicts(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  left: Record<string, any> = {},
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  right: Record<string, any> = {}
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
): Record<string, any> {
  const merged = { ...left };
  for (const [key, value] of Object.entries(right)) {
    if (merged[key] == null) {
      merged[key] = value;
    } else if (value == null) {
      continue;
    } else if (
      typeof merged[key] !== typeof value ||
      Array.isArray(merged[key]) !== Array.isArray(value)
    ) {
      throw new Error(
        `field[${key}] already exists in the message chunk, but with a different type.`
      );
    } else if (typeof merged[key] === "string") {
      if (key === "type") {
        // Do not merge 'type' fields
        continue;
      } else if (
        ["id", "name", "output_version", "model_provider"].includes(key)
      ) {
        // Keep the incoming value for these fields if its defined
        if (value) {
          merged[key] = value;
        }
      } else {
        merged[key] += value;
      }
    } else if (typeof merged[key] === "object" && !Array.isArray(merged[key])) {
      merged[key] = _mergeDicts(merged[key], value);
    } else if (Array.isArray(merged[key])) {
      merged[key] = _mergeLists(merged[key], value);
    } else if (merged[key] === value) {
      continue;
    } else {
      console.warn(
        `field[${key}] already exists in this message chunk and value has unsupported type.`
      );
    }
  }
  return merged;
}

export function _mergeLists<Content extends ContentBlock>(
  left?: Content[],
  right?: Content[]
): Content[] | undefined {
  if (left === undefined && right === undefined) {
    return undefined;
  } else if (left === undefined || right === undefined) {
    return left || right;
  } else {
    const merged = [...left];
    for (const item of right) {
      if (
        typeof item === "object" &&
        item !== null &&
        "index" in item &&
        typeof item.index === "number"
      ) {
        const toMerge = merged.findIndex((leftItem) => {
          const isObject = typeof leftItem === "object";
          const indiciesMatch =
            "index" in leftItem && leftItem.index === item.index;
          const idsMatch =
            "id" in leftItem && "id" in item && leftItem?.id === item?.id;
          const eitherItemMissingID =
            !("id" in leftItem) ||
            !leftItem?.id ||
            !("id" in item) ||
            !item?.id;
          return isObject && indiciesMatch && (idsMatch || eitherItemMissingID);
        });
        if (
          toMerge !== -1 &&
          typeof merged[toMerge] === "object" &&
          merged[toMerge] !== null
        ) {
          merged[toMerge] = _mergeDicts(
            merged[toMerge] as Record<string, unknown>,
            item as Record<string, unknown>
          ) as Content;
        } else {
          merged.push(item);
        }
      } else if (
        typeof item === "object" &&
        item !== null &&
        "text" in item &&
        item.text === ""
      ) {
        // No-op - skip empty text blocks
        continue;
      } else {
        merged.push(item);
      }
    }
    return merged;
  }
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function _mergeObj<T = any>(
  left: T | undefined,
  right: T | undefined
): T {
  if (!left && !right) {
    throw new Error("Cannot merge two undefined objects.");
  }
  if (!left || !right) {
    return left || (right as T);
  } else if (typeof left !== typeof right) {
    throw new Error(
      `Cannot merge objects of different types.\nLeft ${typeof left}\nRight ${typeof right}`
    );
  } else if (typeof left === "string" && typeof right === "string") {
    return (left + right) as T;
  } else if (Array.isArray(left) && Array.isArray(right)) {
    return _mergeLists(left, right) as T;
  } else if (typeof left === "object" && typeof right === "object") {
    return _mergeDicts(left, right) as T;
  } else if (left === right) {
    return left;
  } else {
    throw new Error(
      `Can not merge objects of different types.\nLeft ${left}\nRight ${right}`
    );
  }
}

/**
 * Represents a chunk of a message, which can be concatenated with other
 * message chunks. It includes a method `_merge_kwargs_dict()` for merging
 * additional keyword arguments from another `BaseMessageChunk` into this
 * one. It also overrides the `__add__()` method to support concatenation
 * of `BaseMessageChunk` instances.
 */
export abstract class BaseMessageChunk<
  TStructure extends MessageStructure = MessageStructure,
  TRole extends MessageType = MessageType
> extends BaseMessage<TStructure, TRole> {
  abstract concat(chunk: BaseMessageChunk): BaseMessageChunk<TStructure, TRole>;

  static isInstance(obj: unknown): obj is BaseMessageChunk {
    if (!super.isInstance(obj)) {
      return false;
    }
    // Check if obj is an instance of BaseMessageChunk by traversing the prototype chain
    let proto = Object.getPrototypeOf(obj);
    while (proto !== null) {
      if (proto === BaseMessageChunk.prototype) {
        return true;
      }
      proto = Object.getPrototypeOf(proto);
    }
    return false;
  }
}

export type MessageFieldWithRole = {
  role: MessageType;
  content: MessageContent;
  name?: string;
} & Record<string, unknown>;

export function _isMessageFieldWithRole(
  x: BaseMessageLike
): x is MessageFieldWithRole {
  return typeof (x as MessageFieldWithRole).role === "string";
}

export type BaseMessageLike =
  | BaseMessage
  | MessageFieldWithRole
  | [MessageType, MessageContent]
  | string
  /**
   * @deprecated Specifying "type" is deprecated and will be removed in 0.4.0.
   */
  | ({
      type: MessageType | "user" | "assistant" | "placeholder";
    } & BaseMessageFields &
      Record<string, unknown>)
  | SerializedConstructor;

/**
 * @deprecated Use {@link BaseMessage.isInstance} instead
 */
export function isBaseMessage(
  messageLike?: unknown
): messageLike is BaseMessage {
  return typeof (messageLike as BaseMessage)?._getType === "function";
}

/**
 * @deprecated Use {@link BaseMessageChunk.isInstance} instead
 */
export function isBaseMessageChunk(
  messageLike?: unknown
): messageLike is BaseMessageChunk {
  return BaseMessageChunk.isInstance(messageLike);
}
